package org.dromara.common.idempotent.aspectj;

import cn.dev33.satoken.SaManager;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：防止重复提交(参考美团GTIS防重系统)
 */
@Aspect
public class RepeatSubmitAspect {

    /**
     * 定义一个ThreadLocal用于缓存键值对的键
     */
    private static final ThreadLocal<String> KEY_CACHE = new ThreadLocal<>();

    /**
     * 在执行有重复提交注解的方法前执行
     *
     * @param point 切点
     * @param repeatSubmit 重复提交注解
     * @throws Throwable 抛出异常
     */
    @Before("@annotation(repeatSubmit)")
    public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
        // 如果注解不为0 则使用注解数值 将间隔时间转换为毫秒
        long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());

        // 确保间隔时间不小于1秒
        if (interval < 1000) {
            throw new ServiceException("重复提交间隔时间不能小于'1'秒");
        }
        HttpServletRequest request = ServletUtils.getRequest();
        // 将方法参数转换为字符串
        String nowParams = argsArrayToString(point.getArgs());

        // 请求地址（作为存放cache的key值）
        String url = request.getRequestURI();

        // 唯一值（没有消息头则使用请求地址） 从请求头获取唯一值
        String submitKey = StringUtils.trimToEmpty(request.getHeader(SaManager.getConfig().getTokenName()));

        // 生成最终的唯一标识
        submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
        // 唯一标识（指定key + url + 消息头）
        // 缓存键值
        String cacheRepeatKey = GlobalConstants.REPEAT_SUBMIT_KEY + url + submitKey;
        // 如果键不存在，则插入并设置过期时间
        if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
            KEY_CACHE.set(cacheRepeatKey);
        } else {
            // 提示信息
            String message = repeatSubmit.message();
            // 处理提示信息
            if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
                message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
            }
            throw new ServiceException(message);
        }
    }

    /**
     * 处理完请求后执行
     * 在方法执行成功返回后执行的切面方法
     * 该方法主要用于处理重复提交的场景，根据返回结果判断是否需要删除缓存中的key
     *
     * @param joinPoint 切点 切入点对象，用于获取方法执行的相关信息
     * @param repeatSubmit 重复提交的注解对象，包含重复提交相关的配置信息
     * @param jsonResult 方法执行后的返回结果，用于判断是否需要删除缓存中的key
     */
    @AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
        // 判断返回结果是否为R类型泛型对象
        if (jsonResult instanceof R<?> r) {
            try {
                // 成功则不删除redis数据 保证在有效时间内无法重复提交
                // 如果方法执行成功，则不需要删除缓存中的数据，以防止在有效时间内重复提交
                if (r.getCode() == R.SUCCESS) {
                    return;
                }
                // 删除缓存中的对象，以允许下一次成功提交
                RedisUtils.deleteObject(KEY_CACHE.get());
            } finally {
                // 确保移除缓存中的key，避免内存泄漏
                KEY_CACHE.remove();
            }
        }
    }

    /**
     * 拦截异常操作
     * 处理方法抛出异常的切面方法
     * 当被通知的方法抛出异常时执行此方法主要用于处理重复提交的异常情况
     *
     * @param joinPoint 切点 切入点对象，用于获取方法执行的细节
     * @param repeatSubmit 重复提交注解对象，包含重试提交的配置信息
     * @param e         异常对象，表示方法执行中抛出的异常
     */
    @AfterThrowing(value = "@annotation(repeatSubmit)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception e) {
        // 删除缓存中的键值，以确保下次提交时会重新生成
        RedisUtils.deleteObject(KEY_CACHE.get());
        // 移除缓存中的键值，以确保下次提交时会重新生成
        KEY_CACHE.remove();
    }

    /**
     * 参数拼装
     * 将对象数组转换为字符串
     * <p>
     * 该方法主要用于将传入的参数数组转换为单个字符串，用于日志记录或其他需要将对象集合表示为字符串的场景
     * 它使用空格分隔参数，并且仅包含非空且未被过滤掉的对象
     *
     * @param paramsArray 对象数组，包含了待转换的参数对象
     * @return 转换后的字符串，格式为各个参数的字符串表示，使用空格分隔
     */
    private String argsArrayToString(Object[] paramsArray) {
        // 使用StringJoiner来高效地拼接字符串，初始分隔符为空格
        StringJoiner params = new StringJoiner(" ");

        // 如果参数数组为空，则直接返回空字符串
        if (ArrayUtil.isEmpty(paramsArray)) {
            return params.toString();
        }

        // 遍历参数数组，将非空且未被过滤的对象转换为字符串并添加到params中
        for (Object o : paramsArray) {
            // 检查对象是否非空且未被过滤
            if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
                // 将对象转换为JSON格式的字符串并添加到参数列表中
                params.add(JsonUtils.toJsonString(o));
            }
        }
        return params.toString();
    }

    /**
     * 判断是否需要过滤的对象。
     * 主要用于判断传入的参数是否为需要特殊处理的类型，如文件上传相关的对象或HTTP请求/响应对象
     *
     * @param o 对象信息。待判断的对象
     * @return 如果是需要过滤的对象，则返回true；否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        // 获取对象的类信息
        Class<?> clazz = o.getClass();

        // 检查对象是否为数组类型，如果是，判断其元素类型是否为MultipartFile
        if (clazz.isArray()) {
            return MultipartFile.class.isAssignableFrom(clazz.getComponentType());

            // 检查对象是否为Collection类型，如果是，遍历其元素判断是否为MultipartFile
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }

            // 检查对象是否为Map类型，如果是，遍历其值判断是否为MultipartFile
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.values()) {
                return value instanceof MultipartFile;
            }
        }

        // 最终检查对象是否为MultipartFile、HttpServletRequest、HttpServletResponse或BindingResult类型
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
               || o instanceof BindingResult;
    }

}
